﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using Looptribe.WinRT.Toolkit.Controls.CalendarPrimitives;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Data;

namespace Looptribe.WinRT.Toolkit.Controls
{
    [TemplatePart(Name = ElementMainView, Type = typeof(Grid))]
    [TemplatePart(Name = ElementDayTitleTemplate, Type = typeof(DataTemplate))]
    public class Calendar : Control
    {
        #region Template parts

        private const string ElementMainView = "MainView";

        private const string ElementDayTitleTemplate = "DayTitleTemplate";

        private DataTemplate _dayTitleTemplate;

        private Grid _mainView;

        internal Grid MainView
        {
            get { return _mainView; }
            private set
            {
                // TODO: Detach event handler

                _mainView = value;

                /*if (_mainView != null)
                {
                    _mainView.PointerExited += MonthView_PointerExited;
                }*/
            }
        }
        #endregion

        internal const int RowsPerMonth = 7;

        internal const int NumberOfDaysPerWeek = 7;

        internal const int ColumnsPerMonth = 7;

        #region CurrentDate
        public DateTime CurrentDate
        {
            get { return (DateTime)GetValue(CurrentDateProperty); }
            set { SetValue(CurrentDateProperty, value); }
        }

        public static readonly DependencyProperty CurrentDateProperty =
            DependencyProperty.Register(
                "CurrentDate",
                typeof(DateTime),
                typeof(Calendar),
                new PropertyMetadata(DateTime.Now, OnCurrentDateChanged));

        private static void OnCurrentDateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var c = (Calendar) d;
            c.Update();
        }
        #endregion

        #region FirstDayOfWeek
        /// <summary>
        /// Gets or sets the day that is considered the beginning of the week.
        /// </summary>
        /// <value>
        /// A <see cref="T:System.DayOfWeek" /> representing the beginning of
        /// the week. The default is <see cref="F:System.DayOfWeek.Sunday" />.
        /// </value>
        public DayOfWeek FirstDayOfWeek
        {
            get { return (DayOfWeek)GetValue(FirstDayOfWeekProperty); }
            set { SetValue(FirstDayOfWeekProperty, value); }
        }

        public static readonly DependencyProperty FirstDayOfWeekProperty =
            DependencyProperty.Register(
            "FirstDayOfWeek",
            typeof(DayOfWeek),
            typeof(Calendar),
            new PropertyMetadata(DateTimeHelper.CurrentDateFormat.FirstDayOfWeek, OnFirstDayOfWeekChanged));

        /// <summary>
        /// FirstDayOfWeekProperty property changed handler.
        /// </summary>
        /// <param name="d">Calendar that changed its FirstDayOfWeek.</param>
        /// <param name="e">The DependencyPropertyChangedEventArgs.</param>
        private static void OnFirstDayOfWeekChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var c = (Calendar) d;

            if (IsValidFirstDayOfWeek(e.NewValue))
            {
                c.Update();
            }
            else
            {
                throw new ArgumentOutOfRangeException("d", "Invalid FirstDayOfWeek");
            }
        }

        /// <summary>
        /// Inherited code: Requires comment.
        /// </summary>
        /// <param name="value">Inherited code: Requires comment 1.</param>
        /// <returns>Inherited code: Requires comment 2.</returns>
        private static bool IsValidFirstDayOfWeek(object value)
        {
            var day = (DayOfWeek)value;

            return day == DayOfWeek.Sunday
                || day == DayOfWeek.Monday
                || day == DayOfWeek.Tuesday
                || day == DayOfWeek.Wednesday
                || day == DayOfWeek.Thursday
                || day == DayOfWeek.Friday
                || day == DayOfWeek.Saturday;
        }
        #endregion FirstDayOfWeek

        #region Events

        public IEnumerable<ICalendarEvent> Events
        {
            get { return (IEnumerable<ICalendarEvent>)GetValue(EventsProperty); }
            set { SetValue(EventsProperty, value); }
        }

        public static readonly DependencyProperty EventsProperty =
            DependencyProperty.Register(
                "Events",
                typeof(object), // should be typeof(IEnumerable<ICalendarEvent>) but WinRT is bugged
                typeof(Calendar),
                new PropertyMetadata(null, OnEventsChanged));

        private static void OnEventsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var c = (Calendar)d;

            var oldCollection = e.OldValue as INotifyCollectionChanged;
            if (oldCollection != null)
                oldCollection.CollectionChanged -= c.OnEventsCollectionChanged;

            var newCollection = e.NewValue as INotifyCollectionChanged;
            if (newCollection != null)
                newCollection.CollectionChanged += c.OnEventsCollectionChanged;

            c.Update();
        }

        private void OnEventsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            Update();
        }

        #endregion

        #region ItemTemplate

        public static readonly DependencyProperty ItemTemplateProperty =
            DependencyProperty.Register(
                "ItemTemplate",
                typeof(DataTemplate),
                typeof(Calendar),
                null);

        public DataTemplate ItemTemplate
        {
            get { return (DataTemplate)GetValue(ItemTemplateProperty); }
            set { SetValue(ItemTemplateProperty, value); }
        }

        #endregion

        public event EventHandler<CalendarDayEventArgs> DayTapped;

        private readonly System.Globalization.Calendar _calendar = new CultureInfo("en-US").Calendar;

        private readonly CalendarDay[] _days = new CalendarDay[(RowsPerMonth - 1) * ColumnsPerMonth];

        public Calendar()
        {
            DefaultStyleKey = typeof (Calendar);
        }

        private void CellTapped(object sender, Windows.UI.Xaml.Input.TappedRoutedEventArgs e)
        {
            var control = sender as CalendarDay;
            if (control != null)
            {
                var data = control.DataContext as DayData;
                if (data != null)
                {
                    OnDayTapped(new CalendarDayEventArgs(data.Date, data.Events));
                    e.Handled = true;
                }
            }
        }

        protected virtual void OnDayTapped(CalendarDayEventArgs e)
        {
            var evt = DayTapped;
            if (evt != null)
                evt(this, e);
        }

        #region Templating

        protected override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            MainView = GetTemplateChild(ElementMainView) as Grid;
            _dayTitleTemplate = GetTemplateChild(ElementDayTitleTemplate) as DataTemplate;

            PopulateGrid();

            Update();
        }

        private void PopulateGrid()
        {
            if (MainView == null) return;

            for (var i = 0; i < ColumnsPerMonth; ++i)
            {
                if (_dayTitleTemplate != null)
                {
                    var cell = (FrameworkElement) _dayTitleTemplate.LoadContent();
                    cell.SetValue(Grid.RowProperty, 0);
                    cell.SetValue(Grid.ColumnProperty, i);
                    MainView.Children.Add(cell);
                }
            }

            for (var row = 1; row < RowsPerMonth; ++row)
                for (var col = 0; col < ColumnsPerMonth; ++col)
                {
                    var cell = new CalendarDay();
                    cell.SetValue(Grid.RowProperty, row);
                    cell.SetValue(Grid.ColumnProperty, col);
                    cell.SetBinding(CalendarDay.ItemTemplateProperty, new Binding {Source = ItemTemplate});
                    cell.Tapped += CellTapped;
                    _days[(row - 1) * ColumnsPerMonth + col] = cell;
                    MainView.Children.Add(cell);
                }
        }

        private void Update()
        {
            if (MainView == null) return;
            UpdateTitles();
            UpdateDays();
        }

        private void UpdateTitles()
        {
            for (var childIndex = 0; childIndex < ColumnsPerMonth; childIndex++)
            {
                var daytitle = MainView.Children[childIndex] as FrameworkElement;
                if (daytitle != null)
                {
                    daytitle.DataContext = CultureInfo.CurrentUICulture.DateTimeFormat.DayNames[
                            (childIndex + (int)FirstDayOfWeek) % NumberOfDaysPerWeek];
                }
            }
        }

        private void UpdateDays()
        {
            var count = _days.Length;
            var firstDayOfMonth = new DateTime(CurrentDate.Year, CurrentDate.Month, 1);
            var iterator = firstDayOfMonth;
            if (DateTimeHelper.CompareYearMonth(firstDayOfMonth, DateTime.MinValue) > 0)
            {
                iterator = _calendar.AddDays(firstDayOfMonth, -GetPreviousMonthDayCount(firstDayOfMonth));
            }
            for (var i = 0; i < count; ++i)
            {
                var cell = _days[i];
                ICalendarEvent[] events = null;
                if (Events != null)
                {
                    var iteratorCopy = iterator;
                    events = Events.Where(a => DateTimeHelper.DiscardTime(a.DateTime) == DateTimeHelper.DiscardTime(iteratorCopy)).ToArray();
                }

                cell.DataContext = new DayData(iterator, iterator.Month == CurrentDate.Month, events);
                cell.ChangeVisualState(true);

                if (DateTime.Compare(DateTime.MaxValue.Date, iterator) > 0)
                {
                    iterator = _calendar.AddDays(iterator, 1);
                }
                else
                {
                    // iterator is equal to the DateTime.MaxValue, so there
                    // are no trailing days
                    for (var k = i + 1; k < count; k++)
                    {
                        cell = _days[i];
                        cell.DataContext = null;
                    }
                    return;
                }
            }
        }

        private int GetPreviousMonthDayCount(DateTime firstDayOfMonth)
        {
            var day = _calendar.GetDayOfWeek(firstDayOfMonth);
            var result = ((day - FirstDayOfWeek + NumberOfDaysPerWeek) % NumberOfDaysPerWeek);
            return result > 0 ? result : NumberOfDaysPerWeek;

        }

        #endregion
    }
}
